defmodule Neo4jDb do

	@moduledoc """
	    %{
	        "labels-added" => 1, "nodes-created" => 1,
	        "properties-set" => 1, "nodes-deleted" => 1,
	        "relationships-created" => 1,
	        "relationships-deleted" => 2,
	    }
	"""

	require Logger
	@conn :neo4j_conn
	@update "properties-set"
	@insert "nodes-created"
	@delete "nodes-deleted"
	@rel_create "relationships-created"
	@rel_del "relationships-deleted"

	def preload() do
		"""
		    MATCH (n)
		    OPTIONAL MATCH (n)-[r]->()
		    RETURN count(n.prop) + count(r.prop)
		"""
		|> query()
	end

	def transaction(fun) when is_function(fun, 0) do
		Bolt.Sips.transaction(
			get_conn(),
			fn (conn) ->
				put(conn)
				try do
					fun.()
				after
					erase()
				end
			end
		)
	end

	def get_all(cql, params \\ %{}, default \\ []) do
		conn = get_conn()
		with {:ok, resp} <- run(conn, cql, params) do
			resp.results
			|> case do
					 nil ->
						 default
					 result ->
						 result
				 end
		else
			{:error, error} ->
				Logger.error("get_all Error, reason: #{inspect(error)}, #{cql}")
				raise TipError, "unexpected_error"
		end
	end


	# 不检查 是否存在的,自己调用的
	def get_one(cql, params \\ %{}, default \\ []) do
		conn = get_conn()
		with {:ok, resp} <- run(conn, cql, params) do
			resp
			|> get_first()
			|> case do
					 nil ->
						 default
					 result ->
						 result
				 end
		else
			{:error, error} ->
				Logger.error("get_one Error, reason: #{inspect(error)}, #{cql}")
				raise TipError, "unexpected_error"
		end
	end

	# 要检查值是否存在的, 不存在 throw 异常
	def get_one!(cql, params \\ %{}, opts \\ []) do
		conn = get_conn()
		with {:ok, resp} <- run(conn, cql, params) do
			info = get_first(resp)
			if info == nil do
				case opts[:tip] do
					nil ->
						raise TipError, "input_error"
					v ->
						raise TipError, v
				end
			end
			info
		else
			{:error, error} ->
				Logger.error("get_one! Error, reason: #{inspect(error)}, #{cql}")
				raise TipError, "unexpected_error"
		end
	end


	def insert(cql, params \\ %{}, opts \\ []) do
		query!(cql, @insert, params, opts)
	end

	def update(cql, params \\ %{}, opts \\ []) do
		query!(cql, @update, params, opts)
	end

	def delete(cql, params \\ %{}, opts \\ []) do
		query!(cql, @delete, params, opts)
	end

	def delete_rel(cql, params \\ %{}, opts \\ []) do
		query!(cql, @rel_del, params, opts)
	end

	def create_rel(cql, params \\ %{}, opts \\ []) do
		query!(cql, @rel_create, params, opts)
	end

	def write(cql, params, opts \\ []) do
		query!(cql, [@rel_create, @rel_del, @insert, @update, @delete], params, opts)
	end

	def query(cql, params \\ %{}, opts \\ []) do
		conn = get_conn()
		with {:ok, resp} <- run(conn, cql, params) do
			get_first(resp)
		else
			{:error, error} ->
				Logger.error("query Error, reason: #{inspect(error)}, #{cql}")
				nil
		end
	end


	def query!(cql, method, params \\ %{}, opts \\ [tip: "not_exists"]) do
		conn = get_conn()
		with {:ok, resp} <- run(conn, cql, params),
				 true <- success?(resp, method, opts) do
			get_first(resp)
		else
			false ->
				Logger.error("query! Failed, #{inspect(params)}, #{cql}")
				case opts[:tip] do
					nil ->
						raise TipError, "failed"
					v ->
						raise TipError, v
				end
			{:error, error} ->
				Logger.error("query! Error, reason: #{inspect(error)}, #{cql}, #{inspect(params)}")
				raise TipError, "unexpected_error"
		end
	end

	defp run(conn, cql, params \\ %{}) do
		AppConfigure.fetch(:show_sql) &&
			Logger.debug("\n#{cql}\n#{inspect(params)}")
		apply(Bolt.Sips, :query, [conn, cql, params])
	end

	def put(conn) do
		Process.put(@conn, conn)
	end

	def get_conn() do
		pid = Process.get(@conn)
		with true <- Kernel.is_map(pid),
				 DBConnection <- Map.get(pid, :__struct__),
				 status = DBConnection.status(pid),
				 true <- status != :error do
			pid
		else
			pid ->
				if pid == nil || pid == false || (is_pid(pid) && !Process.alive?(pid)) do
					pid = apply(Bolt.Sips, :conn, [])
					is_pid(pid) || raise "Failed To Get Neo4j Conn, #{inspect(pid)}"
					pid
				else
					pid
				end
		end
	end

	def get_first(resp) do
		apply(Bolt.Sips.Response, :first, [resp])
	end

	@methods [@insert, @update, @rel_del, @rel_create, @delete]
	defp success?(resp, method, opts \\ []) do
		%{:stats => stats, :results => results} = resp
		cond do
			stats == [] ->
				false
			is_list(method) ->
				Enum.reduce_while(
					method,
					false,
					fn (m, acc) ->
						if Map.get(stats, m, 0) > 0 do
							{:halt, true}
						else
							{:cont, acc}
						end
					end
				)
			method in @methods ->
				Map.get(stats, method, 0) > 0
			true ->
				results != []
		end
	end

	def erase() do
		Process.delete(@conn)
	end

end
